package com.pinfly.common.http;

import java.io.UnsupportedEncodingException;
import java.nio.charset.UnsupportedCharsetException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.SimpleTimeZone;

import javax.servlet.ServletRequest;

/**
 * Provides static Http utility methods.
 * 
 */
public class HttpUtil
{
    private static SimpleDateFormat httpFormat;
    // Constructors

    // The Default character encoding for HTTP
    private static final String DEFAULT_CHARACTER_ENCODING = "UTF-8";

    /**
     * This class isn't meant to be instantiated.
     */
    private HttpUtil ()
    {
    }

    /**
     * This returns a string containing the date and time in <var>date</var>
     * formatted according to a subset of RFC-1123. The format is defined in the
     * HTTP/1.0 spec (RFC-1945), section 3.3, and the HTTP/1.1 spec (RFC-2068),
     * section 3.3.1. Note that Date.toGMTString() is close, but is missing the
     * weekday and supresses the leading zero if the day is less than the 10th.
     * Instead we use the SimpleDateFormat class.
     * 
     * @param date the date and time to be converted
     * @return a string containg the date and time as used in http
     */
    public final static String httpDate (Date date)
    {
        if (httpFormat == null)
        {
            // Insure that this is done once per class usage and never
            // at the same time.
            synchronized (HttpUtil.class)
            {
                if (httpFormat == null)
                {
                    httpFormat = new SimpleDateFormat ("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
                    // Some versions of JDK 1.1.x are bugged in that their GMT
                    // uses daylight savings time... Therefore we use our own
                    // timezone definition
                    httpFormat.setTimeZone (new SimpleTimeZone (0, "GMT"));
                }
            }
        }

        return httpFormat.format (date);
    }

    /**
     * Retrieves the path from a relative URI. The path is everything from the
     * left of the URI until a ';', '?', or '#' in that order. If no path
     * terminator is found, then the entire URI is returned. For additional
     * information, see the following <a
     * href="http://www.faqs.org/rfcs/rfc2068.html">HTTP 1.1 RFC</a>.
     * 
     * @param relativeURI The URI to parse.
     * @return The path part of the URI.
     * @throws NullPointerException if relativeURI is null
     * 
     */
    public final static String getPath (String relativeURI)
    {
        if (relativeURI == null)
        {
            throw new NullPointerException ("relativeURI can not be null!");
        }

        if (relativeURI.length () == 0)
        {
            return "";
        }

        int endIndex = relativeURI.indexOf (';');
        if (endIndex == -1)
        {
            endIndex = relativeURI.indexOf ('?');
            if (endIndex == -1)
            {
                endIndex = relativeURI.indexOf ('#');
            }
        }

        if (endIndex == -1)
        {
            // Didn't find any of the characters that would
            // end the path. Just use the end of the string
            endIndex = relativeURI.length ();
        }

        return relativeURI.substring (0, endIndex);
    }

    /**
     * Retrieves the query part from a relative URI. The query is everything to
     * the right of a '?' upto a '#' or the end of the URI. For additional
     * information, see the following <a
     * href="http://www.faqs.org/rfcs/rfc2068.html">HTTP 1.1 RFC</a>.
     * 
     * @param relativeURI The URI to parse.
     * @return The query part of the URI. Null is returned if '?' does not
     *         exist, or if '#' is found before '?'.
     * @throws NullPointerException if relativeURI is null
     */
    public final static String getQuery (String relativeURI)
    {
        if (relativeURI == null)
        {
            throw new NullPointerException ("relativeURI can not be null!");
        }

        if (relativeURI.length () == 0)
        {
            return null;
        }

        int startIndex = relativeURI.indexOf ('?');
        if (startIndex == -1)
        {
            return null;
        }
        startIndex++; // Get past the '?'

        int endIndex = relativeURI.indexOf ('#');
        if (endIndex == -1)
        {
            endIndex = relativeURI.length ();
        }

        if (endIndex < startIndex)
        {
            // There was a '#' before the '?', which is illegal
            return null;
        }
        return relativeURI.substring (startIndex, endIndex);
    }

    /**
     * This method urlencodes the given string. This method is here for symmetry
     * reasons and just calls java.net.URLEncoder.encode() with UTF-8 encoding.
     * 
     * @param str the string to encode
     * @return the url-encoded string
     * @throws UnsupportedCharsetException if the UTF-8 charset could not be
     *             used to encode the string
     */
    public final static String URLEncode (String str)
    {
        String result = null;

        if (str == null)
            return result;

        try
        {
            result = java.net.URLEncoder.encode (str, DEFAULT_CHARACTER_ENCODING);
        }
        catch (UnsupportedEncodingException e)
        {
            UnsupportedCharsetException u = new UnsupportedCharsetException (DEFAULT_CHARACTER_ENCODING);
            u.initCause (e); // Wrap the original exception
            throw u;
        }

        return result;
    }

    /**
     * This method decodes the given urlencoded string. This method is here for
     * symmetry reasons and just calls java.net.URLDecoder.decode() with UTF-8
     * encoding.
     * 
     * @param str the url-encoded string to decode
     * @return the decoded string
     * @throws IllegalArgumentException if the given string is not formatted
     *             correctly
     * @throws UnsupportedCharsetException if the UTF-8 charset could not be
     *             used to decode the string
     */
    public final static String URLDecode (String str)
    {
        String result = null;
        if (str == null)
            return result;

        try
        {
            result = java.net.URLDecoder.decode (str, DEFAULT_CHARACTER_ENCODING);
        }
        catch (UnsupportedEncodingException e)
        {
            UnsupportedCharsetException u = new UnsupportedCharsetException (DEFAULT_CHARACTER_ENCODING);
            u.initCause (e); // Wrap the original exception
            throw u;
        }
        return result;
    }

    /**
     * Decode a reply string.
     * 
     * @deprecated Use {@link #decodeParameters(String)} instead.
     */
    public static HashMap decodeReply (String query) throws Exception
    {
        return decodeParameters (query);
    }

    /**
     * Turns a string of the form "name1=value1&name2=value2&name3=value3" into
     * an Hashmap of name/value pairs. The names and values are urldecoded. The
     * keys are converted into uppercase. If the string contains a key without
     * an equal sign, the hashmap will contain that key, and it will have a
     * value of blank.
     * 
     * <pre>
     * 
     *  Example:
     *  input string is &quot;&amp;name1&amp;=value2&amp;&amp;name2=&amp;name3=Hello&amp;&quot;
     * 
     *  The hashmap would contain the following:
     *    key     value
     *   NAME1     &quot;&quot; ----- blank
     *   NAME2     &quot;&quot; ----- blank
     *   NAME3     Hello
     * 
     * </pre>
     * 
     * Notice that the extra ampersands are ignored, and that the "=value2" is
     * discarded since it does not have a key. The value is trimmed to remove
     * any leading or trailing spaces.
     * 
     * Caveat: names must be unique
     * 
     * @param query String encoded based on URL rules
     * @return HashMap of name/value pairs
     * @exception Exception If the urldecoding of the name or value fails
     */
    public static HashMap decodeParameters (String query) throws Exception
    {
        String key = null;
        String value = null;

        if (query == null || query.length () == 0)
            return new HashMap ();

        int start = 0;
        if (query.charAt (0) == '&')
            start = 1;
        int idx = start - 1;
        int cnt = 1;
        int looper;
        while ((idx = query.indexOf ('&', idx + 1)) != -1)
            cnt++;
        HashMap nameValues = new HashMap (cnt);

        for (idx = start, looper = 0; looper < cnt; looper++)
        {
            int eq = query.indexOf ('=', idx);
            int end = query.indexOf ('&', idx);

            if (end == -1)
                end = query.length ();

            if (eq == -1 || eq >= end)
            {
                // No equal sign found, or an equal sign found after the
                // ampersand. Get the key, and set the value to empty.
                eq = end;

                key = query.substring (idx, end);
                value = "";
            }
            else
            {
                key = query.substring (idx, eq);
                value = query.substring (eq + 1, end);
                if (value != null)
                {
                    value = value.trim ();
                }
            }

            // Blank keys will not be placed in the results
            if (!key.equals (""))
            {
                nameValues.put (URLDecode (key).toUpperCase (), URLDecode (value));
            }

            idx = end + 1;
        }

        return nameValues;
    }

    /**
     * Convert the parameters in a ServletRequest to a Map. This is similar to
     * decodeParameters except it starts with a ServletRequest object instead of
     * parsing a query string. Like decodeParameters, it converts the parameter
     * names to upper case before storing them in the map.
     * <p>
     * This method differs from the standard J2EE method
     * ServletRequest.getParameterMap in the following ways:
     * <ul>
     * <li>The parameter names are converted to upper case before being put in
     * the map.</li>
     * <li>The map values are of type <code>String</code>, not
     * <code>String[]</code>. For multi-valued parameters, only the first value
     * is stored.</li>
     * <li>The returned Map is not immutable.</li>
     * </ul>
     * 
     * @param request The request to get the parameters from
     * @return a Map of parameters and their values
     */
    public static HashMap decodeRequest (ServletRequest request)
    {
        HashMap map = new HashMap ();
        Enumeration names = request.getParameterNames ();
        while (names.hasMoreElements ())
        {
            String name = (String) names.nextElement ();
            String value = request.getParameter (name);
            if (value != null)
            {
                value = value.trim ();
            }
            if (value == null)
                value = "";
            map.put (name.toUpperCase (), value);
        }
        return map;
    }

    /**
     * Return the domain associated with a <code>ServletRequest</code>. The
     * domain is generally the last two components of the server name. For
     * example, the domain associated with www.yahoo.com is yahoo.com. The
     * domain name is useful for being assigned to the document.domain variable
     * of an html page.
     * 
     * @param req the request whose domain is to be determined
     * @return the domain component
     */
    public static String getDomain (ServletRequest req)
    {
        String domain = null;
        String server = req.getServerName ();

        // Look for the second to last .
        int dot = server.lastIndexOf ('.', server.lastIndexOf ('.') - 1);
        if (dot == -1)
        {
            // There weren't two periods - take the whole server name as
            // the domain name
            domain = server;
        }
        else
        {
            // Take everything after the second .
            domain = server.substring (dot + 1);
        }
        return domain;
    }

}
